package neuralNetwork;

import java.util.List;
import java.util.Random;

import neuralModel.activationFunction.IActivationFunction;
import neuralModel.neuron.ClampedInputSensor;
import neuralModel.neuron.INeuron;
import neuralModel.neuron.ISignal;
import neuralModel.neuron.Neuron;

public class NeuralNetworkUtils
{
	private NeuralNetworkUtils()
	{
	}

	public static String INNER_LAYER_NODE_NAME_FORMAT = "node_%d_%d";

	public static INeuralNetwork buildFeedForwardNetwork(String[] inputNames, String[] outputNames,
	    int[] innerLayerSizes, IActivationFunction activationFunction, double initialSignalWeights)
	{
		INeuralNetwork neuralNetwork = null;

		// inner layers + input and output layer
		int layerCount = innerLayerSizes.length + 2;
		neuralNetwork = new NeuralNetwork(layerCount);

		buildInputLayer(neuralNetwork, inputNames);
		buildInnerLayers(neuralNetwork, innerLayerSizes, activationFunction);
		buildOutputLayer(neuralNetwork, outputNames, activationFunction);

		setupFeedFowardConnections(neuralNetwork, initialSignalWeights);

		addBiasToNetwork(neuralNetwork, initialSignalWeights);

		return neuralNetwork;
	}

	private static void buildInputLayer(INeuralNetwork neuralNetwork, String[] inputNames)
	{
		INeuron neuron;
		for (String name : inputNames)
		{
			neuron = new ClampedInputSensor(name);
			neuralNetwork.addNeuron(neuron, 0);
		}
	}

	private static void buildOutputLayer(INeuralNetwork neuralNetwork, String[] outputNames,
	    IActivationFunction activationFunction)
	{
		int outputlayerIndex = neuralNetwork.getLayerCount() - 1;
		INeuron neuron;
		for (String name : outputNames)
		{
			neuron = new Neuron(name, activationFunction);
			neuralNetwork.addNeuron(neuron, outputlayerIndex);
		}
	}

	private static void buildInnerLayers(INeuralNetwork neuralNetwork, int[] innerLayerSizes,
	    IActivationFunction activationFunction)
	{
		INeuron neuron;
		for (int i = 0; i < innerLayerSizes.length; i++)
		{
			for (int j = 0; j < innerLayerSizes[i]; j++)
			{
				neuron = new Neuron(String.format(INNER_LAYER_NODE_NAME_FORMAT, i, j), activationFunction);
				neuralNetwork.addNeuron(neuron, i + 1);
			}
		}
	}

	public static void setupFeedFowardConnections(INeuralNetwork neuralNetwork, double intialWeight)
	{
		int totalLayers = neuralNetwork.getLayerCount();
		for (int i = 0; i < totalLayers - 1; i++)
		{
			FullyConnectNetworkLayers(neuralNetwork, i, i + 1, intialWeight);
		}
	}

	public static void FullyConnectNetworkLayers(INeuralNetwork neuralNetwork, int sourceLayerIndex,
	    int targetLayerIndex, double intialWeight)
	{
		if (neuralNetwork != null)
		{
			if (sourceLayerIndex < neuralNetwork.getLayerCount() && targetLayerIndex < neuralNetwork.getLayerCount())
			{
				List<INeuron> sourceLayer = neuralNetwork.getLayer(sourceLayerIndex);
				List<INeuron> targetLayer = neuralNetwork.getLayer(targetLayerIndex);

				for (INeuron source : sourceLayer)
				{
					for (INeuron target : targetLayer)
					{
						neuralNetwork.connectNeurons(source, target, intialWeight);
					}
				}
			}
			else
			{
				throw new IllegalArgumentException("Invalid Network Layer Specified");
			}
		}
		else
		{
			throw new IllegalArgumentException("Null Network Specified");
		}
	}

	public static void addBiasToNetwork(INeuralNetwork neuralNetwork, double initialWeight)
	{
		INeuron biasNeuron = new ClampedInputSensor("bias");
		neuralNetwork.addNeuron(biasNeuron, 0);
		biasNeuron.setOutputValue(1.0);

		for (INeuron neuron : neuralNetwork.getNeuronMap().values())
		{
			if (neuron.getNetworkLayer() > 0)
				neuralNetwork.connectNeurons(biasNeuron, neuron, initialWeight);
		}
	}

	public static void randomizeNetworkWeights(INeuralNetwork neuralNetwork, double weightLimiter, long seed)
	{
		Random rand = new Random(seed);
		double newWeight;
		for (ISignal signal : neuralNetwork.getSignalList())
		{
			newWeight = (rand.nextDouble() * weightLimiter);
			if (rand.nextInt(2) == 0)
			{
				signal.setWeight(newWeight);
			}
			else
			{
				signal.setWeight(-newWeight);
			}
		}
	}

}
